import java.rmi.*;
import java.rmi.registry.*;
import java.net.*;
import java.util.*;
import java.awt.Point;

/**
A CardsServer instance keeps track of a game, in addition to providing some game controls.

A server may only be in two modes: master, or slave.
In master mode, the server keeps track of who the players are, and can receive external connections to connect new players.  The master server handles processing of player actions, and sends out information about state changes to the slave servers.
In slave mode, the server relays player actions to the master server.  A slave server may also do some processing of its own to increase responsiveness, but this processing may be overrided by information sent to it by the master server.

Note: this may be modified in the future to allow more complex server architectures.
* 
* @author Ricky Vincent
* @author William Jarman
*/
public class CardsServer extends java.rmi.server.UnicastRemoteObject implements ServerInterface {
	
	// networking variables
	private boolean MASTER;
	private static final String RMI_NAME = "ipcards";
	private static final String uid = new java.rmi.dgc.VMID().toString();
	
	// master variables
	private Registry localregistry;
	private HashMap<String, ServerInterface> slaves;
	
	// slave variables
	private Registry remoteregistry;
	private ServerInterface masterServer;
	private int masterPort;
	public int port = 1099;
	
	
	// game variables
	private Room room = new Room(new Table(new Deck("PlayingCards")));
	//private List<Player> players = new ArrayList<Player>();	
	private Player player;
	/* private Map<Player,Set<Card>> selectedCards = new HashMap<PlayerData,Set<Card>>(); */		
	//private Set<Card> selectedCards = new HashSet<Card>(); 
	private PlayerData pd;
	
	private static int snapX = 5;
	private static int snapY = 5;
	private static int snapRatio = 4;
	
	public CardsServer() throws java.rmi.RemoteException {
		this(1099);
	}
	
	public CardsServer(int port) throws java.rmi.RemoteException {
		masterMode(port);
	}
	
	/**
	Changes this server to slave mode, and connects to the master server at the given url.  The table will be cleared, and new table information will be copied from the master server.  If this fails for any reason, it reverts back to it's previous state.
	
	If in master mode, this call will only succeed if it currently does not have any slaves.  If there are slaves, it will throw an exception.  To change a master server with clients to slave mode, you first have to manually call remove slave for each connected slave.
	*/
	public void slaveMode(String url, int port) throws RemoteException {
		MASTER = false;
		String masterUrl = url;
		masterPort = port;
		
		
		try {
			localregistry.unbind(RMI_NAME);
			
// 		} catch(MalformedURLException mue){
// 			System.out.println(mue);
		} catch(NotBoundException nbe){
			System.out.println(nbe);
			nbe.printStackTrace();
		}
		
		try {
			remoteregistry = LocateRegistry.getRegistry(url,port);
			masterServer = (ServerInterface)remoteregistry.lookup(RMI_NAME);
			
// 			masterServer = (ServerInterface)java.rmi.Naming.lookup("rmi://"+url+":"+port+"/"+RMI_NAME);
			
		} catch(NotBoundException nbe){
			System.out.println(nbe);
			nbe.printStackTrace();
			return;
// 		} catch(MalformedURLException mue){
// 			System.out.println(mue);
// 			return;
		}
		
		try{
			masterServer.addSlave(getUid(), this);
		}
		catch(NullPointerException npe){
			System.out.println("OMFG!!! masterServer is null holy shit... \n" + npe);
		}
	}
	
	/**
	Changes this server to master mode.  If it was already in master mode, this has no effect.  If it was in client mode, it first disconnects from its master server.  Preserves table information.
	
	Note: if any cards were pseudoanonymous at the time this was called, they will now be permanently pseudoanonymous, so may not behave correctly.
	*/
	public void masterMode(int port) throws java.rmi.RemoteException {
		
		if(!MASTER) {
			//disconnect
		}
		
		MASTER = true;
		
		this.port = port;
		slaves = new HashMap<String, ServerInterface>();
		
/*		System.out.println("current java.rmi.server.hostname:"+System.getProperty("java.rmi.server.hostname"));
		System.setProperty("java.rmi.server.hostname","192.168.1.104");
		System.out.println("current java.rmi.server.hostname:"+System.getProperty("java.rmi.server.hostname"));*/
		localregistry = LocateRegistry.createRegistry(port);
		
// 		try {
			localregistry.rebind(RMI_NAME, this);
			System.out.println("Bound to rmiregistry on port " + port);
			
// 		} catch(MalformedURLException mue){
// 			System.out.println(mue);
// 		}
		
		System.out.println("Currently bound in local registry:");
		for (String s: localregistry.list()) {
			System.out.println("\t"+s);
		}
		
	}
	
	/**
	Adds a slave server.  When in Client mode, this call is simply relayed to the Master server.
	*/
	public void addSlave(String uid, ServerInterface slave) throws java.rmi.RemoteException {
		System.out.println("Slave Added: " + uid);
		
		if(slaves.containsValue(slave)){
			System.err.println("Slave already connected");
		} else {
			slaves.put(uid, slave);
		}		
	}
	
	/**
	Removes a slave server.  When in slave mode, this call is simply relayed to the Master server.
	* 
	* Should this be a non-remotely callable method?
	*/
	public void removeSlave(String uid) throws java.rmi.RemoteException {
		
		if(MASTER) {
			if(slaves.containsKey(uid)) {
				slaves.remove(uid);
			} else{
				System.err.println("removeSlave: Slave does not exist");
			}
		} else{
			masterServer.removeSlave(uid);
		}
	}	
	
	
	/**
	Returns a list of the slave servers currently connected.  When in Slave mode, this call is simply relayed to the Master server.
	*/
	public List<ServerInterface> getClients() throws java.rmi.RemoteException {
		return (List<ServerInterface>)slaves.values();
	}
	
	
	public void addPlayer(Player p) {
		this.player = p;
		pd = new PlayerData(p, this);
		player.setPlayerListener(pd);
		player.alert();
	}
	
	/**
	 * Tests if a {@link Card} is currently selected.
	 * 
	 * @param card The <code>Card</code> to test.
	 * @return <code>true</code> if the specified <code>Card</code> is selected, otherwise <code>false</code>.
	 */
	public boolean isSelectedCard(Card card) {
		return pd.isSelectedCard(card);
	}
	
	private int findHighestZ(Card selected) {
		return pd.findHighestZ(selected);
	}

	
	/**
	 * Returns the {@link Room}.
	 * 
	 * @return the <code>Room</code>.
	 */
	public Room getRoom() {
		return room;
	}

	/**
	 * @return the snap
	 */
	public int getSnapX() {
		return snapX;
	}

	/**
	 * @param snap the snap to set
	 */
	public static void setSnapY(int snap) {
		snapY = snap / snapRatio;
	}
	
	/**
	 * @return the snap
	 */
	public int getSnapY() {
		return snapY;
	}

	/**
	 * @param snap the snap to set
	 */
	public static void setSnapX(int snap) {
		snapX = snap / snapRatio;
	}
	
	public void setSnap(boolean s) {
		pd.setSnap(s);
	}
	
	//PlayerListener methods
	public void hold()throws java.rmi.RemoteException {
		pd.hold();
	}
	/*
	public void moveBy(Point point)throws java.rmi.RemoteException {
		pd.moveBy(point);
	}*/
	
	public void release()throws java.rmi.RemoteException {
		pd.release();
	}
	
	/*public void flip()throws java.rmi.RemoteException {
		pd.flip();
	}*/
	
	public void select(Set<Card> cards)throws java.rmi.RemoteException {
		pd.select(cards);
	}
	
	public Table getTable(){
		return room.getTable();
	}
	
	public void dropSelection(Set<Card> cards) throws java.rmi.RemoteException {
		//unimplemented
	};
	
	public void updateServer() {
		if(MASTER){
			for(ServerInterface si: slaves.values()){
				try{
					si.updateSlave(getCardDatas());
				}
				catch(RemoteException re){
					System.out.println(re);
				}
			}
		} else{
			try{
				
				
				masterServer.receiveUpdate(getCardDatas());
			}
			catch(RemoteException re){
				System.out.println(re);
			}
		}
		player.alert();
	}
	
	public void updateSlave(List<CardData> cds)throws java.rmi.RemoteException{
		System.out.println("updateslave: GOD NO!!");
		setCardDatas(cds);
		player.alert();
	}
	
	public void receiveUpdate(List<CardData> cds) throws java.rmi.RemoteException{
		//this is horrendous - for the love of god fix it
		System.out.println("receiveupdate: GOD NO!!");
		setCardDatas(cds);
		updateServer();
		player.alert();
	}
	
	private List<CardData> getCardDatas() {
		System.out.println("getcarddatas: GOD NO!!");
		List<Card> cards = room.getTable().getDeck().getDeck();
		List<CardData> cds = new ArrayList<CardData>();
		for (Card c: cards) {
			cds.add(c.getCardData());
		}
		return cds;
	}
	
	private void setCardDatas(List<CardData> cds) {
		System.out.println("setcarddatas: GOD NO!!");
		Deck d = room.getTable().getDeck();
		for (CardData cd: cds) {
			int id = cd.ID;
			d.getCardFromID(id).setCardData(cd);
		}
	}
	
	/**
	 * Flips selected cards. 
	 * If this is a slave, then passed to the master.
	 * If this is a master, then passed to all slaves.
	 */
	public void flip() {
		
		if (MASTER) {
			
			for(ServerInterface si: slaves.values()){
				try {
					si.slaveFlip(pd.getSelectedCardIds());
				} catch(RemoteException re){
					System.out.println(re);
				}
			}
		
		} else { //Slave
			
			try {
				masterServer.masterFlip(getUid(), pd.getSelectedCardIds());
			} catch (RemoteException e) {
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * Called by slaves on the master to flip cards.
	 * 
	 * @param ids The ID's of the cards to flip.
	 * @param uid The unique ID of the slave that is sending this request.
	 */
	public void masterFlip(String uid, int[] ids) throws java.rmi.RemoteException {
		
		Deck deck = room.getTable().getDeck();
		
		for (int id: ids) {
			deck.getCardFromID(id).toggleFaceUp();
		}
		player.alert();
		
		//send flip to all connected slaves (apart from the one that sent it)
		for (String suid: slaves.keySet()) {
			if(!uid.equals(suid)) {
				slaves.get(suid).slaveFlip(ids);
			}
		}			
	}
	
	/**
	 * Called by the master on the slaves to update the flipped status of cards on the slaves.
	 * 
	 * @param ids The ID's of the cards to flip.
	 */
	public void slaveFlip(int[] ids) throws java.rmi.RemoteException {
	
		Deck deck = room.getTable().getDeck();
		
		for (int id: ids) {
			deck.getCardFromID(id).toggleFaceUp();
		}
		player.alert();
	}
	
	/**
	 * Moves selected cards by the amount specified. 
	 * If this is a slave, then passed to the master.
	 * If this is a master, then passed to all slaves.
	 * 
	 * @param point The amount to move the selected cards by.
	 */
	public void moveBy(Point point) {
		
		if (MASTER) {
			
			for(ServerInterface si: slaves.values()){
				try {
					si.slaveMoveBy(pd.getSelectedCardIds(), point);
				} catch(RemoteException re){
					System.out.println(re);
				}
			}
		
		} else { //Slave
			
			try {
				masterServer.masterMoveBy(getUid(), pd.getSelectedCardIds(), point);
			} catch (RemoteException e) {
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * Called by slaves on the master to move cards.
	 * 
	 * @param ids The ID's of the cards to move.
	 * @param uid The unique ID of the slave that is sending this request.
	 * @param point The amount to move the selected cards by.
	 */
	public void masterMoveBy(String uid, int[] ids, Point point) throws java.rmi.RemoteException {
		
		Deck deck = room.getTable().getDeck();
		
		for (int id: ids) {
			deck.getCardFromID(id).moveBy(point);
		}
		player.alert();
		
		//send movement to all connected slaves (apart from the one that sent it)
		for (String suid: slaves.keySet()) {
			if(!uid.equals(suid)) {
				slaves.get(suid).slaveMoveBy(ids, point);
			}
		}			
	}
	
	/**
	 * Called by the master on the slaves to update the position of the cards on the slaves.
	 * 
	 * @param ids The ID's of the cards to flip.
	 * @param point The amount to move the selected cards by.
	 */
	public void slaveMoveBy(int[] ids, Point point) throws java.rmi.RemoteException {
	
		Deck deck = room.getTable().getDeck();
		
		for (int id: ids) {
			deck.getCardFromID(id).moveBy(point);
		}
		player.alert();
	}
	
	

	/**
	 * @return the uid
	 */
	public static String getUid() {
		return uid;
	}
}



